<?php

/**
 * MNet Library 
 * Copyright (C) 2006-2008 Catalyst IT Ltd (http://www.catalyst.net.nz)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    mnet
 * @subpackage core
 * @author     Catalyst IT Ltd
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL
 * @copyright  (C) 2006-2008 Catalyst IT Ltd http://catalyst.net.nz
 * @copyright  (C) portions from Moodle, (C) Martin Dougiamas http://dougiamas.com
 *
 */


require_once MNET_DIR.'/mnet_extension.php';

/**
 * This extension is a core extension used to handle XML-RPC "system" methods
 * such as listing method signatures etc. For this reason it needs to interact both
 * with the MNet core and with other extensions to glean their signatures.
 * 
 * The system extension is also responsible for handling keyswap operations and ping
 * requests.
 * 
 * Note the camel-case method names due to XML-RPC convention for system methods.
 * 
 */
class mnet_extension_system extends mnet_extension {

    /**
     * The system extension implementation of check_call_access()
     * always allows access
     * 
     * @return bool true
     * @see mnet_extension#check_call_access()
     */
    public function check_call_access($client, $method) {
        return true;
    }

    /**
	 * Retrieve the remote system's public key by calling its system/keyswap method
	 * 
	 * Note the result from this method cannot necessarily be trusted because it is an
	 * unencrypted call. It is better to use a trusted means to rekey
	 * 
	 * @param mnet_peer $peer the peer record (at least with applicationid and wwwroot) to call
	 * @return string the public key as a string
     */
    public function get_remote_public_key(mnet_peer $peer) {
		global $MNET, $MNET_APPLICATION;
		static $pk_cache;

		if (isset($pk_cache[$peer->get_wwwroot()])) {
			// we have a cache!
			return $pk_cache[$peer->get_wwwroot()];
		}

		$mnetrequest = new mnet_client();
		$mnetrequest->set_method('system/keyswap');
		$mnetrequest->add_param($MNET->get_local_wwwroot(), 'string');
		$mnetrequest->add_param($MNET->get_public_key_cert(), 'string');

		// For 1.0 (or unknown) peers, we send the applicationid, for MNet 2.0 we send our server path
    if ($peer->get_protocol() == 1.0) {
        $mnetrequest->add_param(mnet_application::get_appid_for_path($MNET_APPLICATION->get_server_path()), 'string');
    }
    else {
        $mnetrequest->add_param(mnet_application::get_server_path(), 'string');
    }

		try {
			$response = $mnetrequest->send_unencrypted($peer);
		}
		catch (mnet_xmlrpc_fault $e) {
			$MNET_APPLICATION->log(MNET_LOG_ERR, 'Remote key fetch failed: '.$e);
			return false;
		}

		$public_certificate = $response;
		$credentials = array();
		if (strlen(trim($public_certificate))) {
		    $credentials = $MNET->parse_certificate($public_certificate);

			if ($peer->get_wwwroot() != $credentials['wwwroot']) {
				$MNET_APPLICATION->log(MNET_LOG_ERR, 'Request for PK returned PK for different URI - '.$credentials['wwwroot'].' needs to be equal to '.$peer->get_wwwroot());
				return false;
			}
		}
		else {
			$MNET_APPLICATION->log(MNET_LOG_ERR, 'Request for PK returned empty response');
			return false;
		}

		// Now also send an encrypted ping request. If the remote side has registered us,
		// this will ensure we have the correct protocol version stored for this peer
		$peer->set_public_key($public_certificate);
		if (!$this->ping($peer)) {
		    $MNET_APPLICATION->log(MNET_LOG_NOTICE, 'Failed ping following pubkey fetch for '.$peer);
		}

		// all good
		$pk_cache[$peer->get_wwwroot()] = $public_certificate;
		return $public_certificate;
    }

    /**
     * Send a ping to a peer (encrypted). Returns true on success.
     * 
     * This is the most basic call you can send to a peer, so is suitable
     * not only for checking aliveness, but also to test if you have the right public
     * key for the peer.
     *
     * @param $peer
     * @return bool true on success
     */
    public function ping(mnet_peer $peer) {
        global $MNET_APPLICATION;

        $mnetrequest = new mnet_client();
        try {
			// If they're a protocol 1 peer, there is no system/ping method to call.
			// Instead, we'll call getServices(), which does exist
			if ($peer->get_protocol() == 1) {
				$mnetrequest->set_method('system/listServices');
				$response = $mnetrequest->send($peer);
				return is_array($response);
			}
			else {
				$mnetrequest->set_method('system/ping');
				$response = $mnetrequest->send($peer);
				return $response === 'pong';
			}
        }
        catch (mnet_xmlrpc_fault $e) {
			$MNET_APPLICATION->log(MNET_LOG_ERR, 'Ping failed: '.$e);
            return false;
        }
    }

    /**
     * Returns a list of available methods
     * 
     * @return array a list of methods in mnet/extname/methodname form
     */
    public function rpc_listMethods() {
       mnet_xmlrpc_server::$remote_client;
       $extensions = mnet_extension::get_extensions();
       $methods = array();

       foreach ($extensions as $extension) {
           
       } 
    }

    /**
     * Returns the method signature
     * FIXME clarify
     * 
     * @return unknown_type
     */
    public function rpc_methodSignature() {
        // TODO work out the signature of methods
        return array();
    }

    /**
     * Provides help on calling a method
     * 
     * @return unknown_type
     */
    public function rpc_methodHelp() {
        // TODO implement
        return array();
    }

    /**
     * Returns a list of services...
     * FIXME like what?
     * 
     * @return unknown_type
     */
    public function rpc_listServices() {
        // TODO glean a list of services
        return array();
    }

    /**
     * Accepts a public key from a new remote host and returns the public key for
     * this host. If 'register all hosts' is turned on, it will bootstrap a record
     * for the remote host in the mnet_host table (if it's not already there)
     *
     * TODO revise doc
     *
     * @param string $remote_wwwroot the www-root of the calling client
     * @param string $remote_pubkey the public key of the remote client
     * @param string $remote_path the path to the XML-RPC server on the remote side (or the application ID for MNet 1.0)
     * @return string our public key in string form
     */
    public function rpc_keyswap($remote_wwwroot, $remote_pubkey, $remote_path) {
        global $MNET, $MNET_APPLICATION;

        if (strpos($remote_path, '/') === false) {
            // the remote path is an application ID from a MNet 1.0 host, not a path!
            if ($remote_path == 'drupal') {
                // there is a bug in Moodle that means it will call us with our own ID. Check if Moodle's calling...
                $request = mnet_server::get_current_request();
                if ($request && $request->get_protocol() == null) {
                    // yep, it's actually Moodle, Drupal never supported MNet 1.0
                    $remote_path = 'moodle';
                }
            }
            $remote_path = mnet_application::get_path_for_app($remote_path);
        }
        $MNET_APPLICATION->log(MNET_LOG_DEBUG, 'Received keyswap request from '.$remote_wwwroot.', remote path '.$remote_path);

        // Translate the remote application into a remote into a remote path
        if ($MNET_APPLICATION->should_register_hosts()) {
            $mnet_peer = mnet_peer::bootstrap($remote_wwwroot, $remote_pubkey, $remote_path);
            if ($mnet_peer) {
                $mnet_peer->commit();
            }
        }

        // return the string version of our current public key
        return $MNET->get_public_key_cert();
    }

    /**
     * When received, simply replies with the string 'pong'.
     * This can be used to make sure encryption and/or signing is working.
     * 
     * @return string the static string 'pong'
     */
    public function rpc_ping() {
        return 'pong';
    }
}
